-
Notifications
You must be signed in to change notification settings - Fork 721
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[tracing-attributes] Support for using #[instrument] with async-trait #711
Conversation
Disclaimer: I have absolutely no idea why the test failed on field_filter_events, especially considering it doesn't appears to use #[instrument], and it's the only piece of code I touched. SO maybe some spurious event ? Also: I deleted the test field_filter_events in tracing-subscriber/tests/filter.rs as it appears to be a duplicated of field_filter_events in tracing-subscriber/tests/field_filter.rs. -- Looks like it's #449 ;) -- Aaaaand there it goes ;) |
This is great, thanks for working on it!
Yeah, that's correct, this test occasionally fails spuriously. I hadn't realized there was a duplicate copy of the test --- it was moved to its own module to prevent racy behavior between the test and other tests that might result in this kind of spurious failure. If the version in filter.rs was not deleted, that might explain the spurious failures! If you don't mind, it would be great to open a separate PR removing the duplicate test, so that it's not part of the squashed commit on master for this PR when we merge it? Thanks! |
Wow, did I ran into a race condition with the update of the rust version or is the network "just" flaky ? Guess I will never know: https://github.com/tokio-rs/tracing/runs/661243980 -- |
…rait * Rewrite the block when it looks like there is async_trait involved to wrap the internal async function instead of the wrapper generated by asyc-trait * Rewrite '_self' as 'self' when using #[instrument] jointly with async-trait * Add somes tests * Remove the mention that async-trait is not supported with tracing
This removes the test `field_filter_events` in `tracing-subscriber/tests/filter.rs` as it appears to be a duplicate of `field_filter_events` in `tracing-subscriber/tests/field_filter.rs` (see discussion at #711 (comment)).
tracing-attributes/Cargo.toml
Outdated
|
||
|
||
[dev-dependencies] | ||
tracing = { path = "../tracing", version = "0.1" } | ||
tracing-futures = { path = "../tracing-futures", version = "0.2" } | ||
tokio-test = { version = "0.2.0" } | ||
tracing-core = { path = "../tracing-core", version = "0.1"} | ||
# I'm not a huge fan of adding such a dependency only for tests, any idea ? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/md I think taking a dependency is fine. This PR is meant to address a poor interaction between the two crates, so it makes sense to depend on the crate that previously caused a poor interaction.
Any idea on how to improve this ? |
tracing-attributes/Cargo.toml
Outdated
# I'm not a huge fan of adding such a dependency only for tests, any idea ? | ||
async-trait = "0.1" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe dev dependencies will only be downloaded when running tests, so I don't think this is a huge deal. If it is an issue, we can also move the tests for compatibility with async-trait
to a separate crate that is used only for testing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right, "Dev-dependencies are not used when compiling a package for building, but are used for compiling tests, examples, and benchmarks.
These dependencies are not propagated to other packages which depend on this package."
So it's finally not that big of an issue, as you said. Alas, I never took the time to "read" (instead of looking at it when in need) the cargo book and it looks like I'm still learning the basics of cargo sometimes ^^
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great, let's just add the dev dependency, then. Since this isn't an issue, we can remove the comment.
Sorry, I just haven't gotten the chance to take a close look at the change yet. I'm planning on giving it a more thorough review this afternoon. At a glance, it looks good! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks good overall — the approach used seems correct. I had some style notes, but didn't see any major issues.
tracing-attributes/src/lib.rs
Outdated
@@ -168,6 +168,28 @@ use syn::{ | |||
/// # Ok(()) | |||
/// } | |||
/// ``` | |||
/// | |||
/// It also works with async-trait (and hopefully most of the libraries that does similar things): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we add a link to the async-trait
crate here?
tracing-attributes/tests/async_fn.rs
Outdated
.new_span(span.clone()) | ||
.enter(span.clone()) | ||
.new_span(span2.clone()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we also assert that the spans are created with the correct fields?
tracing-attributes/src/lib.rs
Outdated
// terrible machinery to make async_trait-like cases works | ||
// (following the approach suggested in | ||
// https://github.com/dtolnay/async-trait/issues/45#issuecomment-571245673) | ||
fn is_async_trait(block: &Block, block_is_async: bool) -> Option<String> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit, take it or leave it: In general, I try to reserve is_
for functions that return a boolean. This is returning the name of the async-trait
-generated inner function, if it exists...
tracing-attributes/src/lib.rs
Outdated
== path | ||
.path | ||
.segments | ||
.iter() | ||
.map(|x| x.ident.to_string()) | ||
.collect::<Vec<String>>() | ||
.join("::") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: can we maybe move this long expression to a let
-binding outside the if condition? this is a little hard to follow IMO.
tracing-attributes/src/lib.rs
Outdated
.map(|x| x.ident.to_string()) | ||
.collect::<Vec<String>>() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we could avoid a couple of allocations if we did this by creating a string and used push_str
to append the string slices from the path.segments
iterator, rather than creating a String
from each string slice and collecting them into a Vec
. I don't think this is hot enough that this is terribly important though, so it's fine as-is.
tracing-attributes/src/lib.rs
Outdated
.clone() | ||
.into_iter() | ||
.flat_map(|param| match param { | ||
FnArg::Typed(PatType { pat, .. }) => param_names(*pat), | ||
FnArg::Receiver(_) => Box::new(iter::once(Ident::new("self", param.span()))), | ||
}) | ||
// if we are inside a function generated by async-trait, we should take care to rewrite | ||
// "_self" as "self" for 'user convenience' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the presence of a _self
identifier is another likely marker that async-trait
is in use...should that be part of our heuristic for detecting async-trait
as well?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right that we could do that to assert with more certainty that we are facing a aync_trait-like situation (aka: it is more probable that async_trait is in use if you see the the both pattern fn f(...) Box::pin(f(...))
and _self
) , but this is not necessarily true will all traits, so its absence cannot rule out the use of async_trait. Consider this for example:
#[async_trait]
pub trait Foo {
async fn foo(v: usize) -> ();
}
This foo
method on this trait doesn't take self
, so async_trait won't give us a _self
parameter for that function either.
In the end, if we want to use this variable renaming to be more precise, maybe we can consider that functions with the fn f(...) Box::pin(f(...))
pattern but with self
as an argument are not cases of async_trait, as self
should have been rewritten to ``self. But I wonder if there are other crates that exists and that does similar things without renaming
self` to `_self` or with a different name, and that we don't want to exclude simply because they don't apply the exact same mechanisms as async_trait.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's fair, I think the pattern we're detecting is much more reliable.
@nightmared as a heads-up, I'll probably also want to merge PR #672 at some point in the near future. That branch probably conflicts w/ this one, so depending on which one merges first, you might need to make some changes after rebasing. |
Thanks for the review, I will probably fix these later today. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks great. I had a few additional suggestions, but I'm happy with this as-is (although, let's remove the comment about dev-dependencies).
I was going to ask that we try to wrap the comments here more consistently with the existing comments in this file, but it looks like the line wrapping here is already not consistent. We can fix that later.
tracing-attributes/Cargo.toml
Outdated
# I'm not a huge fan of adding such a dependency only for tests, any idea ? | ||
async-trait = "0.1" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great, let's just add the dev dependency, then. Since this isn't an issue, we can remove the comment.
tracing-attributes/src/lib.rs
Outdated
/// It also works with [async-trait](https://crates.io/crates/async-trait) (a crate that allows | ||
/// async functions on traits, something not currently possible in rust), and hopefully most | ||
/// libraries that exhibit similar behaviors: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nitpick: can we wrap these comments consistently with other comments in this file?
tracing-attributes/src/lib.rs
Outdated
// obtain the list of direct internal functions and the last expression of the block | ||
for stmt in &block.stmts { | ||
if let Stmt::Item(Item::Fn(fun)) = &stmt { | ||
// is the function declared as async ? If so, this is a good candidate, let's keep it |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit:
// is the function declared as async ? If so, this is a good candidate, let's keep it | |
// is the function declared as async? If so, this is a good candidate, let's keep it |
tracing-attributes/src/lib.rs
Outdated
})) = last_expr | ||
{ | ||
if let Expr::Path(path) = outside_func.as_ref() { | ||
// is it a call to `Box::pin()` ? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit
// is it a call to `Box::pin()` ? | |
// is it a call to `Box::pin()`? |
tracing-attributes/src/lib.rs
Outdated
let mut merged_path = String::new(); | ||
for i in 0..path.path.segments.len() { | ||
merged_path.push_str(&path.path.segments[i].ident.to_string()); | ||
if i < path.path.segments.len() - 1 { | ||
merged_path.push_str("::"); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Take it or leave it: this seems like it could be factored out into a helper function that we could also use here
tracing/tracing-attributes/src/lib.rs
Lines 737 to 743 in 9b07f86
let func_name = inside_path | |
.path | |
.segments | |
.iter() | |
.map(|x| x.ident.to_string()) | |
.collect::<Vec<String>>() | |
.join("::"); |
tracing-attributes/src/lib.rs
Outdated
// is it a call to `Box::pin()` ? | ||
let mut merged_path = String::new(); | ||
for i in 0..path.path.segments.len() { | ||
merged_path.push_str(&path.path.segments[i].ident.to_string()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What we're doing here is writing the ident's fmt::Display
output to the end of path_str
. Using to_string()
creates a new String
, which allocates memory for a new string that we then immediately throw away. What if we used write!
instead, like this:
merged_path.push_str(&path.path.segments[i].ident.to_string()); | |
use std::fmt::Write; | |
write!(&mut merged_path, "{}", &path.path.segments[i].ident.to_string()) | |
.expect("writing to a String never fails"); |
tracing-attributes/src/lib.rs
Outdated
} | ||
|
||
if "Box::pin" == merged_path { | ||
// does it takes at least an argument ? (if it doesn't, it's not gonna compile anyway, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nnit
// does it takes at least an argument ? (if it doesn't, it's not gonna compile anyway, | |
// does it takes at least an argument? (if it doesn't, it's not gonna compile anyway, |
tracing-attributes/src/lib.rs
Outdated
if outside_args.is_empty() { | ||
return None; | ||
} | ||
// is the argument to Box::pin a function call itself ? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit:
// is the argument to Box::pin a function call itself ? | |
// is the argument to Box::pin a function call itself? |
tracing-attributes/src/lib.rs
Outdated
.map(|x| x.ident.to_string()) | ||
.collect::<Vec<String>>() | ||
.join("::"); | ||
// is this function directly defined insided the current block ? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit:
// is this function directly defined insided the current block ? | |
// is this function directly defined insided the current block? |
tracing-attributes/src/lib.rs
Outdated
if let Some(internal_fun_name) = | ||
get_async_trait_name(&input.block, input.sig.asyncness.is_some()) | ||
{ | ||
// let's rewrite some statements ! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit:
// let's rewrite some statements ! | |
// let's rewrite some statements! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wonderful, this looks great and I'm going to go ahead and merge it.
Thanks so much for working on this, @nightmared, I think you've just made a lot of folks very happy! :)
Happy to, I wanted to try tracing, and I was frustrated to learn that tracing wasn't working on the very project I wanted to test it on, so I figured that I could take a look ;) |
0.1.8 (May 13, 2020) Added: - Support for using `#[instrument]` on methods that are part of [`async-trait`] trait implementations (#711) - Optional `#[instrument(err)]` argument to automatically emit an event if an instrumented function returns `Err` (#637) Thanks to @ilana and @nightmared for contributing to this release! [`async-trait`]: https://crates.io/crates/async-trait Signed-off-by: Eliza Weisman <eliza@buoyant.io>
0.1.8 (May 13, 2020) Added: - Support for using `#[instrument]` on methods that are part of [`async-trait`] trait implementations (#711) - Optional `#[instrument(err)]` argument to automatically emit an event if an instrumented function returns `Err` (#637) Thanks to @ilana and @nightmared for contributing to this release! [`async-trait`]: https://crates.io/crates/async-trait Signed-off-by: Eliza Weisman <eliza@buoyant.io>
# 0.1.14 (May 14, 2020) ### Added - **log**: When using the [`log`] compatibility feature alongside a `tracing` `Subscriber`, log records for spans now include span IDs (#613) - **attributes**: Support for using `#[instrument]` on methods that are part of [`async-trait`] trait implementations (#711) - **attributes**: Optional `#[instrument(err)]` argument to automatically emit an event if an instrumented function returns `Err` (#637) - Added `#[must_use]` attribute to the guard returned by `subscriber::set_default` (#685) ### Changed - **log**: Made [`log`] records emitted by spans much less noisy when span IDs are not available (#613) ### Fixed - Several typos in the documentation (#656, #710, #715) Thanks to @FintanH, @shepmaster, @inanna-malick, @zekisharif, @bkchr, @majecty, @ilana and @nightmared for contributing to this release! [`async-trait`]: https://crates.io/crates/async-traite! [`log`]: https://crates.io/crates/log
wrapper generated by asyc-trait
Motivation
As outlined in #399, right now tracing-futures (or more precisely, the code generated by
#[instrument]
) is not working well with async-trait. What happens is that async-trait rewrites the trait method as a non-async function, insides of which it pins the async method we want to instrument, and we end up instrumenting the wrapper instead of the async fn.This will allow people to use traits with (seemingly) async functions along with
#[instrument]
.Solution
I decided to follow the discussion at dtolnay/async-trait#45 and see if I could make it working. Right now I would describe its state as "works for me, and should work for everyone".